﻿// -----------------------------------------------------------------------
// <copyright file="Backup.cs" company="JSC Centido">
//  Copyright (c) JSC Centido. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Net.Mime;
using System.Text;
using System.Xml;
using Centido.Core.Dto;

namespace Centido.Core
{
    /// <summary>
    ///   The class that backups data and metadata of the specified domain.
    /// </summary>
    public class BackupTask : TaskBase
    {
        /// <summary>
        ///   The default file name.
        /// </summary>
        public const string DefaultFileName = "backup.zip";

        /// <summary>
        ///   The path to the domain metadata domain inside a zip package.
        /// </summary>
        public const string MetadataDocumentPath = @"/Metadata/domain.xml";

        /// <summary>
        ///   The path template for a table data document inside a zip package.
        /// </summary>
        public const string TableDataDocumentPathTemplate = @"/TableData/{0}.xml";

        /// <summary>
        ///   The default domain components that should be exported.
        /// </summary>
        public const DomainComponents DefaultIncludedComponents = DomainComponents.All;


        /// <summary>
        ///   The file name.
        /// </summary>
        private string _fileName = DefaultFileName;

        /// <summary>
        ///   The set of domain components that should be exported.
        /// </summary>
        private DomainComponents _includedComponents = DefaultIncludedComponents;


        /// <summary>
        ///   Gets the current domain.
        /// </summary>
        public Domain Domain { get; private set; }


        /// <summary>
        ///   Gets or sets the file name that will store the backup.
        /// </summary>
        public string FileName
        {
            get { return _fileName; }
            set 
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _fileName = value;
            }
        }


        /// <summary>
        ///   Gets or sets the set of domain components that should be exported.
        /// </summary>
        /// <example>
        ///   The following example shows how to backup only the schema of the current domain.
        /// <code>
        ///     using System;
        ///     using Centido.Core;
        /// 
        ///     class DomainSample
        ///     {
        ///         static void Main()
        ///         {
        ///             var domainConfig = new DomainConfiguration()
        ///             {
        ///                 DbConnectionString = @"Driver={SQL Server};Server=.\SQLEXPRESS;Database=MyDb;Uid=admin;Pwd=P@ssw0rd;",
        ///                 DbProviderName = "MSSQLSERVER"
        ///             };
        ///         
        ///             var domain = Domain.Build(domainConfig);
        ///         
        ///             domain.DeleteAll();
        /// 
        ///             // Creating a role
        ///             domain.AddRole("my_role");
        /// 
        ///             // Creating a unit
        ///             domain.AddUnit("my_unit");
        ///             
        ///             //Creating a table
        ///             var table = domain.AddTable("my_table");
        ///             table.GrantAll();
        /// 
        ///             //Creating a column
        ///             var column = table.AddColumn("my_col", ColumnType.Integer);
        /// 
        ///             // Inserting a row
        ///             var id = table.InsertRow(column.Value(25));
        /// 
        ///             new BackupTask(domain)
        ///               { FileName = "c:\\temp\\backup2.zip", IncludedComponents = DomainComponents.Schema }
        ///               .Run();
        /// 
        ///             domain.DeleteAll();
        /// 
        ///             new RestoreTask(domain)
        ///             { FileName = "c:\\temp\\backup2.zip" }.Run();
        /// 
        ///             // Showing the restored objects
        ///             Console.WriteLine("The table was found = {0}", domain.GetTable("my_table") != null);
        ///             Console.WriteLine("The column was found = {0}", domain.GetTable("my_table")
        ///               .GetColumn("my_col") != null);  
        ///             Console.WriteLine("The row was found = {0}", domain.GetTable("my_table")
        ///               .ContainsRow(id, DataSource.InternalTable)); 
        ///             Console.WriteLine("The role was found = {0}", domain.GetRole("my_role") != null);
        ///             Console.WriteLine("The unit was found = {0}", domain.GetTable("my_unit") != null);    
        ///         }
        ///     }
        /// </code>
        /// </example>
        public DomainComponents IncludedComponents
        {
            get { return _includedComponents; }
            set { _includedComponents = value; }
        }


        /// <summary>
        ///   Creates a new instance of the <see cref="BackupTask"/> class
        /// for the specified domain.
        /// </summary>
        /// <param name="domain">The domain.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="domain"/> is null.</exception>
        public BackupTask(Domain domain)
        {
            if (domain == null)
                throw new ArgumentNullException("domain");

            Domain = domain;
        }


        /// <summary>
        ///   Runs the backup process.
        /// </summary>
        /// <remarks>The current user has to have the permission to change security settings
        /// and the permission to change the schema in order to perform this operation.</remarks>
        /// <example>
        ///   The following example shows how to make a backup.
        /// <code>
        ///     using System;
        ///     using Centido.Core;
        /// 
        ///     class DomainSample
        ///     {
        ///         static void Main()
        ///         {
        ///             var domainConfig = new DomainConfiguration()
        ///             {
        ///                 DbConnectionString = @"Driver={SQL Server};Server=.\SQLEXPRESS;Database=MyDb;Uid=admin;Pwd=P@ssw0rd;",
        ///                 DbProviderName = "MSSQLSERVER"
        ///             };
        ///         
        ///             var domain = Domain.Build(domainConfig);
        ///         
        ///             domain.DeleteAll();
        ///             
        ///             //Creating a table
        ///             var table = domain.AddTable("my_table");
        ///             table.GrantAll();
        /// 
        ///             //Creating a column
        ///             var column = table.AddColumn("my_col", ColumnType.Integer);
        /// 
        ///             // Inserting a row
        ///             var id = table.InsertRow(column.Value(25));
        /// 
        ///             new BackupTask(domain)
        ///               { FileName = "c:\\temp\\backup2.zip" }
        ///               .Run();
        /// 
        ///             domain.DeleteAll();
        /// 
        ///             new RestoreTask(domain)
        ///             { FileName = "c:\\temp\\backup2.zip" }.Run();
        /// 
        ///             // Reading the restored row
        ///             Console.WriteLine("Restored row: my_col = {0}",
        ///               domain.GetTable("my_table").GetColumn("my_col").Read(id).GetInteger());   
        ///         }
        ///     }
        /// </code>
        /// </example>
        public override void Run()
        {
            using (var package = Package.Open(FileName, FileMode.Create))
            {
                // Creating the metadata file
                CreateMetadataDocument(package);

                // Creating the table data files
                CreatesTableDataDocuments(package);
            }
        }


        /// <summary>
        ///   Creates a metadata document.
        /// </summary>
        /// <param name="package">The current sip package.</param>
        private void CreateMetadataDocument(Package package)
        {
            // Sending a diagnostic message
            OnMessageReceived(new TaskMessageReceivedEventArgs(
                string.Format("Processing the metadata.")
            ));

            var metadataPartUri = PackUriHelper.CreatePartUri(new Uri(MetadataDocumentPath, UriKind.Relative));
            var metadataPart = package.CreatePart(metadataPartUri, MediaTypeNames.Text.Xml,
                CompressionOption.Maximum);

            using (var xmlWriter = new XmlTextWriter(metadataPart.GetStream(), Encoding.Unicode))
            {
                var backupDto = PopulateBackup();

                backupDto.WriteXml(xmlWriter);
            }
        }


        /// <summary>
        ///   Populates a data transfer object for the backup.
        /// </summary>
        /// <returns>Returns the populated data transfer object.</returns>
        private BackupDto PopulateBackup()
        {
            var backupDto = new BackupDto 
            {
                CreatedOn = DateTime.Now,
                CreatedBy = Domain.CurrentUser.Name
            };
            
            // Populating the schema
            PopulateSchema(backupDto);

            // Populating security settings
            PopulateSecurity(backupDto);

            // Populating the domain options
            PopulateDomainOptions(backupDto);

            return backupDto;
        }


        /// <summary>
        ///   Populates the domain schema for the specified backup dto.
        /// </summary>
        /// <param name="backupDto">The backup data transfer object.</param>
        private void PopulateSchema(BackupDto backupDto)
        {
            // If there is no need to populate the domain schema
            if ((IncludedComponents & DomainComponents.Schema) != DomainComponents.Schema)
                return;

            // Saving the table list
            foreach (var table in Domain.Tables)
            {
                var tableDto = TableDto.CreateFrom(table);

                // Saving the table columns
                foreach (var column in table.Columns.Where(c => !c.IsSystem))
                {
                    ColumnDto columnDto = null;

                    // If this is an enumeration column
                    if (column is EnumColumn)
                    {
                        var enumColumn = (EnumColumn)column;

                        var enumColumnDto = EnumColumnDto.CreateFrom(enumColumn);

                        // Saving the enumeration members
                        foreach (var enumMember in enumColumn.Members)
                            enumColumnDto.AddMember(EnumMemberDto.CreateFrom(enumMember));

                        columnDto = enumColumnDto;
                    }
                    // If this is a reference column
                    else if (column is RefColumn)
                    {
                        var refColumn = (RefColumn)column;

                        var refColumnDto = RefColumnDto.CreateFrom(refColumn);

                        // Saving the reference targets
                        foreach (var target in refColumn.Targets)
                            refColumnDto.AddTarget(RefTargetDto.CreateFrom(target));

                        columnDto = refColumnDto;
                    }
                    // If this is a big integer column
                    else if (column is BigIntColumn)
                        columnDto = BigIntColumnDto.CreateFrom((BigIntColumn)column);
                    // If this is a binary column
                    else if (column is BinaryColumn)
                        columnDto = BinaryColumnDto.CreateFrom((BinaryColumn)column);
                    // If this is a boolean column
                    else if (column is BoolColumn)
                        columnDto = BoolColumnDto.CreateFrom((BoolColumn)column);
                    // If this is a datetime column
                    else if (column is DateTimeColumn)
                        columnDto = DateTimeColumnDto.CreateFrom((DateTimeColumn)column);
                    // If this is a decimal column
                    else if (column is DecimalColumn)
                        columnDto = DecimalColumnDto.CreateFrom((DecimalColumn)column);
                    // If this is a guid column
                    else if (column is GuidColumn)
                        columnDto = GuidColumnDto.CreateFrom((GuidColumn)column);
                    // If this is a integer column
                    else if (column is IntColumn)
                        columnDto = IntColumnDto.CreateFrom((IntColumn)column);
                    // If this is a small integer column
                    else if (column is SmallIntColumn)
                        columnDto = SmallIntColumnDto.CreateFrom((SmallIntColumn)column);
                    // If this is a string column
                    else if (column is StringColumn)
                        columnDto = StringColumnDto.CreateFrom((StringColumn)column);
                    // If this is a text column
                    else if (column is TextColumn)
                        columnDto = TextColumnDto.CreateFrom((TextColumn)column);

                    if (columnDto != null)
                        tableDto.AddColumn(columnDto);
                }

                // Saving the table indexes
                foreach (var index in table.Indexes)
                    tableDto.AddIndex(IndexDto.CreateFrom(index));
                
                if ((IncludedComponents & DomainComponents.TablePermissions) == DomainComponents.TablePermissions)
                    // Saving the table permissions
                    foreach (var permission in table.Permissions)
                        tableDto.AddPermission(TablePermissionDto.CreateFrom(permission));

                backupDto.AddTable(tableDto);
            }
        }


        /// <summary>
        ///   Populates the domain security settings for the specified backup dto.
        /// </summary>
        /// <param name="backupDto">The backup data transfer object.</param>
        private void PopulateSecurity(BackupDto backupDto)
        {
            // Saving the role list
            if ((IncludedComponents & DomainComponents.Roles) == DomainComponents.Roles)
                foreach (var role in Domain.Roles)
                {
                    var roleDto = RoleDto.CreateFrom(role);

                    // Saving the role users
                    foreach (var user in role.Users)
                        roleDto.AddUser(user.Name);

                    // Savinf the child roles
                    foreach (var childRole in role.ChildRoles)
                        roleDto.AddChildRole(childRole.Name);

                    backupDto.AddRole(roleDto);
                }

            // Saving the unit list
            if ((IncludedComponents & DomainComponents.Units) == DomainComponents.Units)
                foreach (var unit in Domain.Units)
                {
                    var unitDto = UnitDto.CreateFrom(unit);

                    // Saving the unit users
                    foreach (var user in unit.Users)
                        unitDto.AddUser(user.Name);

                    // Saving the unit children
                    foreach (var childUnit in unit.ChildUnits)
                        unitDto.AddChildUnit(childUnit.Name);

                    backupDto.AddUnit(unitDto);
                }

            // Saving the user list
            if ((IncludedComponents & DomainComponents.Users) == DomainComponents.Users)
                foreach (var user in Domain.Users)
                    backupDto.AddUser(UserDto.CreateFrom(user));
        }


        /// <summary>
        ///   Populates the domain options for the specified backup dto.
        /// </summary>
        /// <param name="backupDto">The backup data transfer object.</param>
        private void PopulateDomainOptions(BackupDto backupDto)
        {
            // If there is no need to save domain options
            if ((IncludedComponents & DomainComponents.DomainOptions) != DomainComponents.DomainOptions)
                return;

            backupDto.Options = DomainOptionsDto.CreateFrom(Domain);
        }


        /// <summary>
        ///   Creates table data documents.
        /// </summary>
        /// <param name="package">The current zip package.</param>
        private void CreatesTableDataDocuments(Package package)
        {
            // If there is no need to populate data
            if ((IncludedComponents & DomainComponents.Rows) != DomainComponents.Rows)
                return;

            // Creating a document foreach table
            foreach (var table in Domain.Tables)
                CreateTableDataDocument(package, table);
        }


        /// <summary>
        ///   Creates a document for the table data.
        /// </summary>
        /// <param name="package">The current zip package.</param>
        /// <param name="table"></param>
        private void CreateTableDataDocument(Package package, Table table)
        {
            // Sending a diagnostic message
            OnMessageReceived(new TaskMessageReceivedEventArgs(
                string.Format("Processing '{0}' table.", table.Name)
            ));

            var tableDataPartUri = PackUriHelper.CreatePartUri(new Uri(
                string.Format(TableDataDocumentPathTemplate, table.Name),
                UriKind.Relative));
            var tableDataPart = package.CreatePart(tableDataPartUri, MediaTypeNames.Text.Xml,
                CompressionOption.Maximum);

            using (var xmlWriter = new XmlTextWriter(tableDataPart.GetStream(), Encoding.Unicode))
            {
                var tableDataDto = new TableDataDto
                {
                    TableName = table.Name
                };

                // The column value list
                var columnList = new List<ColumnValue>();

                // The system columns
                var idValue = table.SystemColumns.Id.EmptyValue();
                var ownerValue = table.SystemColumns.Owner.EmptyValue();
                var isActive = table.SystemColumns.IsActive.EmptyValue();
                var rowVersionValue = table.SystemColumns.RowVersion.EmptyValue();
                var createdByValue = table.SystemColumns.CreatedBy.EmptyValue();
                var createdOnValue = table.SystemColumns.CreatedOn.EmptyValue();
                var updatedByValue = table.SystemColumns.UpdatedBy.EmptyValue();
                var updatedOnValue = table.SystemColumns.UpdatedOn.EmptyValue();
                var deletedByValue = table.SystemColumns.DeletedBy.EmptyValue();
                var deletedOnValue = table.SystemColumns.DeletedOn.EmptyValue();

                columnList.Add(idValue);
                columnList.Add(ownerValue);
                columnList.Add(isActive);

                // If the system columns should be included
                if ((IncludedComponents & DomainComponents.SystemColumns)
                    == DomainComponents.SystemColumns)
                {
                    columnList.Add(rowVersionValue);
                    columnList.Add(createdByValue);
                    columnList.Add(createdOnValue);
                    columnList.Add(updatedByValue);
                    columnList.Add(updatedOnValue);
                }

                // If the deleted rows should be included
                if ((IncludedComponents & DomainComponents.DeletedRows)
                    == DomainComponents.DeletedRows)
                {
                    columnList.Add(deletedByValue);
                    columnList.Add(deletedOnValue);
                }

                // The user column values
                var userColumnValues = table.Columns
                    .Where(c => !c.IsSystem).Select(c => c.EmptyValue()).ToList();

                columnList.AddRange(userColumnValues);

                tableDataDto.WriteHandler = w =>
                    {
                        var rowDataDto = new TableRowDataDto();

                        table.ReadAllRows(DataSource.InternalTable,
                            () =>
                            {
                                // If this row was deleted
                                if (isActive.GetBoolean() == false
                                    // .. there no need to include deleted rows
                                    && (IncludedComponents & DomainComponents.DeletedRows) 
                                        != DomainComponents.DeletedRows)
                                    return;

                                // Setting the row system columns
                                rowDataDto.Id = idValue.GetGuid().GetValueOrDefault();
                                rowDataDto.Owner = ownerValue.GetString();

                                // If the system columns should be included
                                if ((IncludedComponents & DomainComponents.SystemColumns)
                                    == DomainComponents.SystemColumns)
                                {
                                    rowDataDto.Version = rowVersionValue.GetGuid();
                                    rowDataDto.CreatedBy = createdByValue.GetString();
                                    rowDataDto.CreatedOn = createdOnValue.GetDateTime();
                                    rowDataDto.UpdatedBy = updatedByValue.GetString();
                                    rowDataDto.UpdatedOn = updatedOnValue.GetDateTime();
                                }

                                // If the deleted rows should be included
                                if ((IncludedComponents & DomainComponents.DeletedRows)
                                    == DomainComponents.DeletedRows)
                                {
                                    rowDataDto.DeletedBy = deletedByValue.GetString();
                                    rowDataDto.DeletedOn = deletedOnValue.GetDateTime();
                                }

                                // Setting the row user columns
                                foreach (var columnValue in userColumnValues)
                                    // If the column value is not null
                                    rowDataDto.AddColumnValue(
                                        columnValue.ColumnName,
                                        // Serializing the column value
                                        columnValue.HasValue ? SerializeColumnValue(columnValue) : null);

                                rowDataDto.WriteXml(w);
                            }, columnList.ToArray());
                    };

                tableDataDto.WriteXml(xmlWriter);
            }
        }


        /// <summary>
        ///   Serializes the specified column value to string.
        /// </summary>
        /// <param name="columnValue">The specified column value.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="columnValue"/> is null.</exception>
        /// <returns>Returns the string representation of the specified column value.</returns>
        private static string SerializeColumnValue(ColumnValue columnValue)
        {
            if (columnValue == null)
                throw new ArgumentNullException("columnValue");
            
            // If this is a binary column
            if (columnValue.Column is BinaryColumn)
                return Encoding.Default.GetString(columnValue.GetBinary());

            // If this is a datetime column
            if (columnValue.Column is DateTimeColumn)
                return columnValue.GetDateTime().GetValueOrDefault()
                    .ToString(CultureInfo.InvariantCulture);

            // If this is a decimal column
            if (columnValue.Column is DecimalColumn)
                return columnValue.GetDecimal().GetValueOrDefault()
                    .ToString(CultureInfo.InvariantCulture);

            // If this is a boolean column
            if (columnValue.Column is BoolColumn)
                return columnValue.GetBoolean().GetValueOrDefault()
                    .ToString(CultureInfo.InvariantCulture);

            // If this is a reference column
            if (columnValue.Column is RefColumn)
                return columnValue.GetGuid().GetValueOrDefault()
                    .ToString();

            // If this is a guid column
            if (columnValue.Column is GuidColumn)
                return columnValue.GetGuid().GetValueOrDefault()
                    .ToString();

            return columnValue.Value.ToString();
        }
    }
}
